package com.lhkj.ct.framework.aspect;


import com.alibaba.fastjson2.JSON;
import com.lhkj.ct.base.annntation.Log;
import com.lhkj.ct.base.buffer.Buffer;
import com.lhkj.ct.base.config.SystemConfig;
import com.lhkj.ct.base.model.LoginUser;
import com.lhkj.ct.base.spring.SpringContextHolder;
import com.lhkj.ct.base.utils.IpUtils;
import com.lhkj.ct.base.utils.ServletUtils;
import com.lhkj.ct.base.utils.ShiroUtil;
import com.lhkj.ct.framework.log.buffer.DefaultBuffer;
import com.lhkj.ct.framework.filter.PropertyPreExcludeFilter;
import com.lhkj.ct.meta.admin.model.entity.TblOperationLog;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;

/**
 * <p>
 *     操作日志aop
 * </p>
 */

@Aspect
@Component
public class OperationLogAop {

    private static final Logger logger = LoggerFactory.getLogger(OperationLogAop.class);

    /** 排除敏感属性字段 */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };

    /** 计算操作消耗时间 */
    private static final ThreadLocal<Long> TIME_THREAD_LOCAL = new NamedThreadLocal<Long>("Cost Time");

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    public void boBefore(JoinPoint joinPoint, Log controllerLog) {
        logger.info("操作日志-进入切面");
        TIME_THREAD_LOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, Log log, final Exception e, Object jsonResult) {
        try {
            long cost = System.currentTimeMillis() - TIME_THREAD_LOCAL.get();
            // 获取当前的用户
            LoginUser loginUser = ShiroUtil.getAuthUserInfo();
            // 请求
            HttpServletRequest request = ServletUtils.getRequest();
            // 是否执行失败
            boolean error = Objects.nonNull(e);
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            // 构建日志消息
            String ipAddr = IpUtils.getIpAddr();
            TblOperationLog operationLog = TblOperationLog.OperationLogBuilder()
                    // 设置消耗时间
                    .costTime(cost)
                    // 描述
                    .title(log.title())
                    // 操作类型
                    .actionType(log.actionType())
                    // 客户端ip(操作ip)
                    .clientIp(ipAddr)
                    // 操作地点
                    .operateLocation(IpUtils.getIpRegion(ipAddr))
                    // 客户端信息
                    .userAgent(request.getHeader("user-agent"))
                    // 请求地址
                    .requestUri(request.getServletPath())
                    // 设置请求方式
                    .httpMethod(request.getMethod())
                    // 应用内类名+方法名
                    .method(className + "." + methodName + "()")
                    // 操作状态（0正常 1异常）
                    .status(error ? 1 : 0)
                    // 错误信息
                    .errorMsg(error ? StringUtils.substring(e.getMessage(), 0, 2000) : null)
                    // 设置操作人类别
                    .operatorType(log.operatorType())
                    // 操作人账号
                    .operatorNo(loginUser.getLoginName())
                    // 操作人名称
                    .operatorName(loginUser.getUserName())
                    // 创建时间
                    .createTime(LocalDateTime.now())
                    .builder();
            // 处理设置注解上的参数、封装请求参数和请求结果
            getControllerMethodDescription(joinPoint, log, operationLog, jsonResult);
            // 将日志放入buffer缓存中，等待消费
            Buffer<String> buffer = SpringContextHolder.getBean(DefaultBuffer.class);
            if (SystemConfig.getPrintInConsole()) {
                logger.info("【请求来源】:{}", operationLog.getClientIp());
                logger.info("【请求地址】:{}", request.getServletPath());
                logger.info("【请求参数】:{}", operationLog.getRequestParams());
                if (operationLog.getStatus() == 0) {
                    logger.info("【请求结果】:{}", operationLog.getJsonResult());
                } else {
                    logger.info("【请求结果】:{}", operationLog.getErrorMsg());
                }
                logger.info("【执行时间】:{}毫秒", operationLog.getCostTime());
            }
            buffer.put(JSON.toJSONString(operationLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            logger.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        } finally {
            TIME_THREAD_LOCAL.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log 日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, TblOperationLog operLog, Object jsonResult) throws Exception {
        // 是否需要保存request，参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息，传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }
        // 是否需要保存response，参数和值
        if (log.isSaveResponseData() && Objects.nonNull(jsonResult)){
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 4000));
        }
    }

    /**
     * 获取请求的参数，放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, TblOperationLog operLog, String[] excludeParamNames) throws Exception {
        Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        String requestMethod = operLog.getHttpMethod();
        // put和post通过表单提交
        if (MapUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
            String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
            operLog.setRequestParams(StringUtils.substring(params, 0, 4000));
        } else {
            operLog.setRequestParams(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 4000));
        }
    }

    /**
     * 请求参数拼装
     */
    private String argsArrayToString(Object[] args, String[] excludeParamNames) {
        StringBuilder params = new StringBuilder();
        if (args != null && args.length > 0) {
            for (Object o : args) {

                if (Objects.nonNull(o) && !isFilterObject(o)) {
                    try {
                        String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
                        params.append(jsonObj).append(" ");
                    } catch (Exception ignored) {
                    }
                }
            }
        }
        return params.toString().trim();
    }

    /**
     * 忽略敏感属性
     */
    public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) {
        return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象，则返回true；否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult;
    }

}
